CloudFront FunctionsはLambda@Edgeより安い。それ本当?!
CX事業本部@大阪の岩田です。先月GAされたCloudFront Functions(以後CF2とします)ですが、従来から利用できたLambda@Edge(以後L@Eとします)と比較して、高速かつ低コストというのがウリの1つになっています。最大実行時間や最大メモリといった制約はありますが、レスポンスヘッダを固定付与するようなライトな処理はCF2のユースケースとしてAWSの公式ドキュメントでも紹介されています。
Customizing at the edge with CloudFront Functions
では今後はレスポンスヘッダの固定付与のような処理は全てCF2で実装すべきなのでしょうか?基本的にはCF2で良いと思いますが、使い方次第ではCF2よりもL@Eの方が低コストになることも考えられるので、なぜこのようなケースが起こり得るのかをご紹介します。
CF2とL@Eの料金体系比較
まず2つのサービスの料金体系を比較してみます。料金は2021/6時点の東京リージョンでの料金となります。
CF2
CF2はシンプルに
- 呼び出し 1,000,000 件あたり 0.10 USD
です。1年間は2,000,000件/月の無料利用枠も存在します。
L@E
L@Eはリクエスト数に応じた課金と、メモリの使用時間に対する課金があり、それぞれ
- リクエスト 1,000,000 件あたり 0.60USD
- GB-秒あたり 0.00005001USD
となります。そして無料利用枠はありません。
この情報だけ見ると、CF2のコストはL@Eの6分の1以下になりそうです。が、実際にはもう少し考慮事項が必要になります。
CF2とL@Eの違い
CF2とL@Eの機能には様々な違いがあります。詳しくは以下のブログを参照して下さい。
今回注目したいのが関数をトリガーするイベントです。CF2はビューワーリクエストとビューワーレスポンスの2つしかサポートしませんが、L@Eはこれら2つに加えてさらにオリジンリクエストとオリジンレスポンスの2つがサポートされています。このビューワーXXXとオリジンXXXの違いですが、ビューワーxxxはCloudFrontのキャッシュヒット有無に関わらず関数実行が実行されるのに対し、オリジンXXXはリクエストされたオブジェクトがCloudFrontのキャッシュにヒットしなかった場合のみ実行されるという特徴があります。オリジンレスポンスをトリガーにL@Eを起動してレスポンスヘッダを書き換えた場合、Cloud Frontは書き換え後のレスポンスをキャッシュします。そしてキャッシュが有効な間は以後の同一オブジェクトに対するリクエストはキャッシュから返却し、オリジンへのリクエストは発生しません。例えばindex.html
というオブジェクトに対して100万件のリクエストが発生し、うち初回の1回のみキャッシュミスが発生するとします。この場合、オリジンレスポンスを使用してレスポンスヘッダを書き換えるとL@Eの起動回数は1回だけで済みますが、ビューワーレスポンスを使用してレスポンスヘッダを書き換えるとL@E(もしくはCF2)は100万回起動することになります。
関数呼び出しの回数が同一であればL@EよりCF2の方が安上がりですが、オリジンレスポンスからトリガーできるL@EはCF2よりも関数呼び出しの回数そのものを削減できる可能性があり、必ずしもL@EよりCF2の方が低コストになるというわけではないのです。
実行回数を比較してみる
理屈は分かったので、実際に動作を比較してみます。
CFのオリジンに静的WEBサイトホスティングを有効化したS3を設定した定番の環境を構築して静的ファイルを配信
- オリジンレスポンスからトリガーしたL@E
- ビューワーレスポンスからトリガーしたCF2
それぞれでレスポンスヘッダを設定しつつ、各関数の実行回数を比較します。
L@Eでレスポンスヘッダを設定した場合
Node.js14xで以下のLambdaを用意してCFのオリジンレスポンスに設定します。L@EとCF2のユースケースとしてよく紹介される、セキュリティ関連のレスポンスヘッダを設定するコードです。
'use strict'; exports.handler = (event, context, callback) => { const response = event.Records[0].cf.response; const headers = response.headers; headers['strict-transport-security'] = [{key: 'Strict-Transport-Security', value: 'max-age=63072000; includeSubdomains; preload'}]; headers['content-security-policy'] = [{key: 'Content-Security-Policy', value: "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'"}]; headers['x-content-type-options'] = [{key: 'X-Content-Type-Options', value: 'nosniff'}]; headers['x-frame-options'] = [{key: 'X-Frame-Options', value: 'DENY'}]; headers['x-xss-protection'] = [{key: 'X-XSS-Protection', value: '1; mode=block'}]; headers['x-response-from'] = [{key: 'X-Response-From', value: 'lae'}]; callback(null, response); };
L@Eからのレスポンスということが分かりやすいようにセキュリティ関連のヘッダに加えてX-Response-From
というヘッダにlae
とセットしています。軽くcurlで動作確認してみます
$ curl https://xxxxxx.cloudfront.net/lae/index.html --dump-header - -s -o /dev/null HTTP/2 200 content-type: text/html content-length: 13 date: Sun, 20 Jun 2021 02:10:56 GMT last-modified: Sat, 19 Jun 2021 11:48:51 GMT etag: "01bcb1fe182a23a65c5efe8326250da8" server: AmazonS3 strict-transport-security: max-age=63072000; includeSubdomains; preload content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none' x-content-type-options: nosniff x-frame-options: DENY x-xss-protection: 1; mode=block x-response-from: lae x-cache: Miss from cloudfront via: 1.1 xxxxxx.cloudfront.net (CloudFront) x-amz-cf-pop: KIX56-C1 x-amz-cf-id: LiFeUzLkJrXDnxgbi1zXLHhCqbRSDCSAakA7Wr4RMMIFIeAeXWeWiQ==
L@Eで設定したセキュリティ関連のヘッダがレスポンスされています。x-cache
はMiss from cloudfront
となっており、キャッシュにヒットしていないことが分かります。再度同じコマンドを実行してみます。
$ curl https://xxxxxx.cloudfront.net/lae/index.html --dump-header - -s -o /dev/null HTTP/2 200 content-type: text/html content-length: 13 date: Sun, 20 Jun 2021 02:10:56 GMT last-modified: Sat, 19 Jun 2021 11:48:51 GMT etag: "01bcb1fe182a23a65c5efe8326250da8" server: AmazonS3 strict-transport-security: max-age=63072000; includeSubdomains; preload content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none' x-content-type-options: nosniff x-frame-options: DENY x-xss-protection: 1; mode=block x-response-from: lae x-cache: Hit from cloudfront via: 1.1 xxxxxx.cloudfront.net (CloudFront) x-amz-cf-pop: KIX56-C1 x-amz-cf-id: TxAcvdc_5Ml98Ljroqh3OpfFT0sx315LzrJqY8gBAADa8vLMx2tKuA== age: 124
先ほどと同様セキュリティ関連のヘッダが設定されていますが、今度はx-cache: Hit from cloudfront
となっており、キャッシュからレスポンスが返却されていることが分かります。つまり。L@Eは起動していないはずです。
続いてabコマンドで10万回ほどリクエストを発行してみます。
$ ab -n 100000 -c 100 https://xxxxxx.cloudfront.net/lae/index.html This is ApacheBench, Version 2.3 <$Revision: 1874286 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking xxxxxx.cloudfront.net (be patient) Completed 10000 requests Completed 20000 requests Completed 30000 requests Completed 40000 requests Completed 50000 requests Completed 60000 requests Completed 70000 requests Completed 80000 requests Completed 90000 requests Completed 100000 requests Finished 100000 requests Server Software: AmazonS3 Server Hostname: xxxxxx.cloudfront.net Server Port: 443 SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128 Server Temp Key: ECDH P-256 256 bits TLS Server Name: xxxxxx.cloudfront.net Document Path: /lae/index.html Document Length: 13 bytes Concurrency Level: 100 Time taken for tests: 68.481 seconds Complete requests: 100000 Failed requests: 0 Total transferred: 74485283 bytes HTML transferred: 1300000 bytes Requests per second: 1460.25 [#/sec] (mean) Time per request: 68.481 [ms] (mean) Time per request: 0.685 [ms] (mean, across all concurrent requests) Transfer rate: 1062.18 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 11 40 6.8 38 118 Processing: 7 29 5.5 30 142 Waiting: 3 11 2.9 10 140 Total: 38 68 8.2 66 184 Percentage of the requests served within a certain time (ms) 50% 66 66% 71 75% 75 80% 76 90% 78 95% 80 98% 87 99% 91 100% 184 (longest request)
しばらくしてからCloudWatchのメトリクスを確認します
L@Eは1回しか実行されていないことが分かります。
CF2でレスポンスヘッダを設定した場合
続いてCF2で同様の処理を実装してみます。関数のコードは以下です。
function handler(event) { var response = event.response; var headers = response.headers; headers['strict-transport-security'] = { value: 'max-age=63072000; includeSubdomains; preload'}; headers['content-security-policy'] = { value: "default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'"}; headers['x-content-type-options'] = { value: 'nosniff'}; headers['x-frame-options'] = {value: 'DENY'}; headers['x-xss-protection'] = {value: '1; mode=block'}; headers['x-response-from'] = {value: 'cf2'}; return response; }
X-Response-From
というヘッダにはcf2
とセットしています。こちらの関数をビューワーレスポンスからトリガーするように設定した後L@Eと同様にcurlで動作確認してみます。
$ curl https://xxxxxx.cloudfront.net/cf2/index.html --dump-header - -s -o /dev/null HTTP/2 200 content-type: text/html content-length: 13 date: Sun, 20 Jun 2021 02:17:02 GMT last-modified: Sat, 19 Jun 2021 11:48:51 GMT etag: "01bcb1fe182a23a65c5efe8326250da8" server: AmazonS3 via: 1.1 xxxxxx.cloudfront.net (CloudFront) content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none' x-response-from: cf2 strict-transport-security: max-age=63072000; includeSubdomains; preload x-xss-protection: 1; mode=block x-frame-options: DENY x-content-type-options: nosniff x-cache: Miss from cloudfront x-amz-cf-pop: NRT51-C3 x-amz-cf-id: deWrm4d4pZqZ0_9hYaAhk2LcyGSU_VAJHgZsvSkhg-j6P1e1uKzbTg==
L@Eと同様にセキュリティ関連のヘッダが設定できていますね。もう1度同じコマンドを実行します。
$ curl https://xxxxxx.cloudfront.net/cf2/index.html --dump-header - -s -o /dev/null HTTP/2 200 content-type: text/html content-length: 13 date: Sun, 20 Jun 2021 02:17:02 GMT last-modified: Sat, 19 Jun 2021 11:48:51 GMT etag: "01bcb1fe182a23a65c5efe8326250da8" server: AmazonS3 via: 1.1 xxxxxx.cloudfront.net (CloudFront) content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none' x-response-from: cf2 strict-transport-security: max-age=63072000; includeSubdomains; preload x-xss-protection: 1; mode=block x-frame-options: DENY x-content-type-options: nosniff x-cache: Hit from cloudfront x-amz-cf-pop: NRT51-C3 x-amz-cf-id: 6LZw6YXuhwAHaj5KaFIXtPTLJyI_1bugEcbY_CVTdenG8vsJuH6UzA==
x-cache: Hit from cloudfront
のレスポンスが返却されており、キャッシュにヒットしていることが分かります。キャッシュヒット人もL@Eと同様のレスポンスヘッダがセットされています。
先程のL@Eと同様abコマンドで10万回ほどリクエストを発行してみます。
$ ab -n 100000 -c 100 https://xxxxxx.cloudfront.net/cf2/index.html This is ApacheBench, Version 2.3 <$Revision: 1874286 $> Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ Licensed to The Apache Software Foundation, http://www.apache.org/ Benchmarking xxxxxx.cloudfront.net (be patient) Completed 10000 requests Completed 20000 requests Completed 30000 requests Completed 40000 requests Completed 50000 requests Completed 60000 requests Completed 70000 requests Completed 80000 requests Completed 90000 requests Completed 100000 requests Finished 100000 requests Server Software: AmazonS3 Server Hostname: xxxxxx.cloudfront.net Server Port: 443 SSL/TLS Protocol: TLSv1.2,ECDHE-RSA-AES128-GCM-SHA256,2048,128 Server Temp Key: ECDH P-256 256 bits TLS Server Name: xxxxxx.cloudfront.net Document Path: /cf2/index.html Document Length: 13 bytes Concurrency Level: 100 Time taken for tests: 69.183 seconds Complete requests: 100000 Failed requests: 0 Total transferred: 73600001 bytes HTML transferred: 1300000 bytes Requests per second: 1445.44 [#/sec] (mean) Time per request: 69.183 [ms] (mean) Time per request: 0.692 [ms] (mean, across all concurrent requests) Transfer rate: 1038.91 [Kbytes/sec] received Connection Times (ms) min mean[+/-sd] median max Connect: 7 40 6.7 38 194 Processing: 8 29 5.5 30 152 Waiting: 4 11 3.4 10 149 Total: 40 69 8.4 67 232 Percentage of the requests served within a certain time (ms) 50% 67 66% 73 75% 76 80% 77 90% 79 95% 81 98% 90 99% 92 100% 232 (longest request)
CloudWatchのメトリクスを確認してみましょう
curlコマンドによるリクエスト×2回 + abコマンドによるリクエスト×10万回で100,002回実行されていることが分かります。
料金比較
オリジンレスポンスはCloudFrontにキャッシュされるという特性から、L@Eの方が実行回数を抑えられることが分かりました。今回の検証にかかった料金を試算してみましょう。
L@Eの料金
今回L@Eのメモリ割り当ては128M、CW Logsから確認した実行時間は80msでした。
- 呼び出しに対する課金:1回 / 100万回 × 0.6USD → 0.0000006USD
- メモリの使用時間に対する課金:(128M/1024M) × (80ms/1000ms) × 0.00005001USD → 0.0000005001USD
合計で 0.0000006 + 0.0000005001 → 0.0000011001USD となる試算です。
CF2の料金
無料利用枠は無視し、実行回数10万2回のうち半端な2回分の実行は無視して計算します。
- 呼び出しに対する課金: 10万回 / 100万回 × 0.10USD → 0.01USD
合計で0.01USDになる試算です。
今回の検証と同等のキャッシュヒット率と実行回数であればCF2よりもL@Eの方が安くなることが分かります。
まとめ
CF2よりもL@Eの方が低コストになる場合があることを紹介しました。そうはいってもCF2の料金は十分に安く、今回の検証にかかったCF2の料金は1円程度です。CF2で処理できる内容であれば基本的にはCF2を選択しつつ、アクセス数=関数の実行回数が大きくなることが想定される場合はL@Eを検討するぐらいの使い方でも問題無さそうです。実際は他にもキャッシュの有効期限やキャッシュヒット率、ブラウザキャッシュを考慮する必要がありますし、L@Eのコールドスタートによるレイテンシへの悪影響にも注意が必要です。単純にコストだけでL@Eを選択するのも良い選択とは言えません。CF2とL@E両者の特性をしっかり抑えた上で適切なサービスを選択するようにしましょう。